import type { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { GetItemCommand, PutItemCommand, ConditionalCheckFailedException,
  UpdateItemCommand, DeleteItemCommand } from "@aws-sdk/client-dynamodb";
import { unmarshall, marshall } from "@aws-sdk/util-dynamodb";

import type { DbService, DocItem, DynamoDbSvcConfig, ExtraData } from "../types";
import { uuid } from "../utils";

const EMPTY_OBJECT = {};

export class DynamoDbService implements DbService {

  private readonly dynamodb: DynamoDBClient;

  private readonly table_name: string;

  private readonly lease: number;

  private readonly ttl: number;

  private readonly client_name: string;

  constructor(config: DynamoDbSvcConfig) {
    this.dynamodb = config.dynamodb;
    this.table_name = config.table_name;
    this.lease = config.lease;
    this.ttl = config.ttl ?? 86400;
    this.client_name = config.client_name ?? "anonymous";
  }


  public readonly getItem = async (id: string): Promise<DocItem | null> => {
    const res = await this.dynamodb.send(new GetItemCommand(
      {
        TableName: this.table_name,
        Key: { id: { S: id } },
        ConsistentRead: true
      }
    ));
    if (!res.Item) {
      return null;
    }
    return unmarshall(res.Item) as DocItem;
  };

  public readonly createNewLock = async (
    id: string,
    extra_data?: ExtraData
  ): Promise<string | null> => {
    try {
      /** revision version number */
      const rvn = uuid();
      await this.dynamodb.send(new PutItemCommand(
        {
          TableName: this.table_name,
          Item: marshall(
            {
              id,
              lease: this.lease,
              rvn,
              expires_at: Math.ceil(Date.now() / 1000) + this.ttl,
              client_name: this.client_name,
              ...(extra_data ? { extra_data } : EMPTY_OBJECT)
            }
          ),
          ConditionExpression: "attribute_not_exists(id)"
        }
      ));
      return rvn;
    }
    catch (error) {
      if (error instanceof ConditionalCheckFailedException) {
        return null;
      }
      else {
        throw error;
      }
    }
  };

  public readonly grabLock = async (
    id: string,
    rvn: string,
    extra_data?: ExtraData
  ): Promise<string | null> => {
    try {
      const new_rvn = uuid();
      await this.dynamodb.send(new UpdateItemCommand(
        extra_data ? {
          TableName: this.table_name,
          Key: marshall({ id }),
          UpdateExpression: "set lease = :lease, rvn = :new_rvn, expires_at = :expires_at, "
            + "extra_data = :extra_data, client_name = :client_name",
          ConditionExpression: "rvn = :rvn",
          ExpressionAttributeValues: marshall(
            {
              ":lease": this.lease,
              ":rvn": rvn,
              ":new_rvn": new_rvn,
              ":expires_at": Math.ceil(Date.now() / 1000) + this.ttl,
              ":extra_data": extra_data ?? null,
              ":client_name": this.client_name
            }
          )
        } : {
          TableName: this.table_name,
          Key: marshall({ id }),
          UpdateExpression: "set lease = :lease, rvn = :new_rvn, expires_at = :expires_at, "
          + "client_name = :client_name",
          ConditionExpression: "rvn = :rvn",
          ExpressionAttributeValues: marshall(
            {
              ":lease": this.lease,
              ":rvn": rvn,
              ":new_rvn": new_rvn,
              ":expires_at": Math.ceil(Date.now() / 1000) + this.ttl,
              ":client_name": this.client_name
            }
          )
        }
      ));
      return new_rvn;
    }
    catch (error) {
      if (error instanceof ConditionalCheckFailedException) {
        return null;
      }
      else {
        throw error;
      }
    }
  };

  public readonly sendHeartBeat = async (
    id: string,
    rvn: string
  ): Promise<string | null> => {
    try {
      const new_rvn = uuid();
      await this.dynamodb.send(new UpdateItemCommand(
        {
          TableName: this.table_name,
          Key: marshall({ id }),
          UpdateExpression: "set rvn = :new_rvn, expires_at = :expires_at",
          ConditionExpression: "rvn = :rvn",
          ExpressionAttributeValues: marshall(
            {
              ":rvn": rvn,
              ":new_rvn": new_rvn,
              ":expires_at": Math.ceil(Date.now() / 1000) + this.ttl
            }
          )
        }
      ));
      return new_rvn;
    }
    catch (error) {
      if (error instanceof ConditionalCheckFailedException) {
        return null;
      }
      else {
        throw error;
      }
    }
  };

  public readonly releaseLock = async (
    id: string,
    rvn: string
  ): Promise<void> => {
    try {
      await this.dynamodb.send(new DeleteItemCommand(
        {
          TableName: this.table_name,
          Key: marshall({ id }),
          ConditionExpression: "attribute_exists(id) and rvn = :rvn",
          ExpressionAttributeValues: marshall({ ":rvn": rvn })
        }
      ));
    }
    catch (error) {
      if (!(error instanceof ConditionalCheckFailedException)) {
        throw error;
      }
    }
  };
}
